package database

import (
	"context"
	"errors"
	"fmt"
	"gitee.com/zhucheer/orange/cfg"
	"gitee.com/zhucheer/orange/internal"
	"gitee.com/zhucheer/orange/logger"
	"github.com/zhuCheer/pool"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"sync"
	"time"
)

var mongoConn *MongoDB

type MongoDB struct {
	dsn      string
	connPool map[string]pool.Pool
	count    int
	lock     sync.Mutex
}

// NewMysql 初始化mysql连接
func NewMongo() DataBase {
	if mongoConn != nil {
		return mongoConn
	}

	mongoConn = &MongoDB{
		connPool: make(map[string]pool.Pool, 0),
	}
	return mongoConn
}

// 注册所有已配置的 mongo
func (mo *MongoDB) RegisterAll() {
	config := cfg.Config.GetMap("database.mongo")

	mo.count = len(config)
	for dd := range config {
		mo.Register(dd)
	}
}

// Register 注册一个mongo配置
func (mo *MongoDB) Register(name string) {
	connUrl := cfg.Config.GetString("database.mongo." + name + ".url")
	connTimeout := cfg.GetInt("database.mongo."+name+".timeout", 10)

	initCap := getDBIntConfig("mongo", name, "initCap")
	maxCap := getDBIntConfig("mongo", name, "maxCap")
	idleTimeout := getDBIntConfig("mongo", name, "idleTimeout")
	connCtx := context.TODO()

	mo.dsn = connUrl
	// connMongo 建立连接
	connMongo := func() (interface{}, error) {
		clientOptions := options.Client().ApplyURI(connUrl).SetConnectTimeout(time.Duration(connTimeout) * time.Second).
			SetServerSelectionTimeout(5 * time.Second).
			SetSocketTimeout(30 * time.Second)

		// connect to MongoDB
		conn, err := mongo.Connect(connCtx, clientOptions)

		if err != nil {
			return nil, err
		}
		return conn, err
	}

	// closeMongo 关闭连接
	closeMongo := func(v interface{}) error {
		err := v.(*mongo.Client).Disconnect(connCtx)
		if err != nil {
			logger.Error("disconnect mongodb conn [%s] error:%v", name, err)
		}
		return nil
	}

	// pingMongo 检测连接连通性
	pingMongo := func(v interface{}) error {
		conn := v.(*mongo.Client)
		return conn.Ping(connCtx, nil)
	}

	// 创建一个连接池
	p, err := pool.NewChannelPool(&pool.Config{
		InitialCap: initCap,
		MaxCap:     maxCap,
		Factory:    connMongo,
		Close:      closeMongo,
		Ping:       pingMongo,
		//连接最大空闲时间，超过该时间的连接 将会关闭，可避免空闲时连接EOF，自动失效的问题
		IdleTimeout: time.Duration(idleTimeout) * time.Second,
	})
	if err != nil {
		logger.Error("register mongodb conn [%s] error:%v", name, err)
		return
	}
	mo.insertPool(name, p)
}

// insertPool 将连接池插入map
func (mo *MongoDB) insertPool(name string, p pool.Pool) {
	if mo.connPool == nil {
		mo.connPool = make(map[string]pool.Pool, 0)
	}

	mo.lock.Lock()
	defer mo.lock.Unlock()
	mo.connPool[name] = p

	hideDsn := hideTcpDsnPasswordLog(mo.dsn)
	internal.ConsoleLog(fmt.Sprintf("create MongoDB pool [%s](%v) success", name, hideDsn))
}

// getDB 从连接池获取一个连接
func (mo *MongoDB) getDB(name string) (conn interface{}, put func(), err error) {
	put = func() {}

	if _, ok := mo.connPool[name]; !ok {
		return nil, put, errors.New("no mongodb connect")
	}

	conn, err = mo.connPool[name].Get()
	if err != nil {
		return nil, put, errors.New(fmt.Sprintf("mongodb get connect err:%v", err))
	}

	put = func() {
		mo.connPool[name].Put(conn)
	}

	return conn, put, nil
}

// putDB 将连接放回连接池
func (mo *MongoDB) putDB(name string, db interface{}) (err error) {
	if _, ok := mo.connPool[name]; !ok {
		return errors.New("no mongodb connect")
	}
	err = mo.connPool[name].Put(db)

	return
}

// GetMongo 获取一个 mongodb连接
func GetMongo(name string) (conn *mongo.Client, put func(), err error) {
	put = func() {}
	if mongoConn == nil {
		return nil, put, errors.New("db connect is nil")
	}
	var vconn interface{}
	vconn, put, err = mongoConn.getDB(name)
	if err != nil {
		return nil, put, err
	}
	conn = vconn.(*mongo.Client)

	return conn, put, nil
}

// GetMongoDB You can get a mongo database object directly
func GetMongoDB(name, db string) (database *mongo.Database, put func(), err error) {
	put = func() {}
	if mongoConn == nil {
		return nil, put, errors.New("db connect is nil")
	}
	var vconn interface{}
	vconn, put, err = mongoConn.getDB(name)
	if err != nil {
		return nil, put, err
	}
	conn := vconn.(*mongo.Client)

	database = conn.Database(db)

	return database, put, nil
}
